// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2016-2018, LomboTech Co.Ltd.
 * Authors:
 *	lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute  it and/or modify it
 * under  the terms of  the GNU General  Public License as published by the
 * Free Software Foundation;  either version 2 of the  License, or (at your
 * option) any later version.
 */

#include <linux/module.h>
#include <linux/component.h>
#include <linux/of_platform.h>

#include <drm/drmP.h>
#include <drm/drm_atomic_helper.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_panel.h>
#include <drm/drm_mipi_dsi.h>
#include <video/mipi_display.h>
#include <video/videomode.h>
#include <drm/drm_modes.h>
#include <drm/drm_bridge.h>
#include "../../../../drivers/gpu/drm/drm_crtc_internal.h"

#include "lombo_drv.h"
#include "lombo_doss.h"
#include "csp/csp_doss_mipi_dsi.h"

struct lombo_mipi_dsi {
	struct drm_connector connector;
	struct drm_encoder encoder;
	struct drm_bridge *bridge;
	struct mipi_dsi_host host;

	struct device *dev;
	struct device *doss_top;
	struct drm_device *drm;
	void __iomem *reg;
	uint32_t id;

	struct lombo_doss_clk *cfg_clk;
	struct lombo_doss_clk *ref_clk;

	unsigned long flag;
	unsigned int lanes;
	enum mipi_dsi_pixel_format format;
	struct lombo_vo_dev_config configs;

	struct drm_panel *panel;
};

static inline struct lombo_mipi_dsi *to_lombo_mipi_dsi_by_enc(
	struct drm_encoder *enc)
{
	return container_of(enc, struct lombo_mipi_dsi, encoder);
}

static inline struct lombo_mipi_dsi *to_lombo_mipi_dsi_by_con(
	struct drm_connector *con)
{
	return container_of(con, struct lombo_mipi_dsi, connector);
}

static inline struct lombo_mipi_dsi *to_lombo_mipi_dsi_by_host(
	struct mipi_dsi_host *h)
{
	return container_of(h, struct lombo_mipi_dsi, host);
}

static void lombo_dsi_timing_check_in(struct lombo_vo_dev_config *vo,
	struct drm_display_mode *mode)
{
	tcon_timings_t *timing = &vo->timing;
	struct videomode vm;

	if (mode->flags & DISPLAY_FLAGS_INTERLACED)
		timing->is_interlace = true;
	else {
		drm_display_mode_to_videomode(mode, &vm);
		timing->field1.vfp = vm.vfront_porch;
		timing->field1.vsw = vm.vsync_len;
		timing->field1.vact = vm.vactive;
		timing->field1.vt = vm.vfront_porch + vm.vsync_len +
					vm.vactive + vm.vback_porch;
		timing->hline.hfp = vm.hfront_porch;
		timing->hline.hsw = vm.hsync_len;
		timing->hline.hact = vm.hactive;
		timing->hline.ht = vm.hfront_porch + vm.hsync_len +
					vm.hactive + vm.hback_porch;
		timing->height = vm.vactive;
		timing->width = vm.hactive;
		timing->dclk_freq = vm.pixelclock;
	}
}

static int lombo_dsi_encoder_atomic_check(struct drm_encoder *enc,
	struct drm_crtc_state *crtc_state,
	struct drm_connector_state *conn_state)
{
	struct lombo_mipi_dsi *dsi = to_lombo_mipi_dsi_by_enc(enc);
	struct drm_display_mode *mode = &crtc_state->mode;
	struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode;
	struct lombo_vo_dev_config *vo = &dsi->configs;
	tcon_mipi_dsi_if_t *dsi_if = &dsi->configs.dsi_if;

	dsi_if->lane_num = dsi->lanes;
	if (dsi->flag & MIPI_DSI_MODE_VIDEO) {
		dsi_if->tx_mode = DSI_VIDEO_MODE;
		vo->dev_if = VO_DEV_MIPI_DSI_VIDEO;
	} else {
		dsi_if->tx_mode = DSI_CMD_MODE;
		vo->dev_if = VO_DEV_MIPI_DSI_CMD;
	}
	if (dsi->flag & MIPI_DSI_MODE_VIDEO_BURST) {
		dsi_if->tx_mode_cfg.video_mode.trans_mode =
					DSI_BURST;
	} else {
		if (dsi->flag & MIPI_DSI_MODE_VIDEO_SYNC_PULSE)
			dsi_if->tx_mode_cfg.video_mode.trans_mode =
					DSI_NON_BURST_PULSE;
		else
			dsi_if->tx_mode_cfg.video_mode.trans_mode =
					DSI_NON_BURST_EVENT;
	}

	switch (dsi->format) {
	case MIPI_DSI_FMT_RGB888:
		dsi_if->tx_fmt = DSI_RGB888;
		break;
	case MIPI_DSI_FMT_RGB666:
		dsi_if->tx_fmt = DSI_RGB666;
		break;
	case MIPI_DSI_FMT_RGB666_PACKED:
		dsi_if->tx_fmt = DSI_RGB666_L;
		break;
	case MIPI_DSI_FMT_RGB565:
		dsi_if->tx_fmt = DSI_RGB565;
		break;
	default:
		DRM_ERROR("dsi format config err\n");
		break;
	}
	lombo_dsi_timing_check_in(vo, mode);
	adjusted_mode->private = (int *)vo;

	return 0;
}

static void lombo_dsi_encoder_atomic_mode_set(
	struct drm_encoder *enc,
	struct drm_crtc_state *crtc_state,
	struct drm_connector_state *conn_state)
{
	struct lombo_mipi_dsi *dsi = to_lombo_mipi_dsi_by_enc(enc);
	struct drm_display_mode *mode = &crtc_state->adjusted_mode;
	struct lombo_vo_dev_config *vo_dev_cfg =
		(struct lombo_vo_dev_config *)(mode->private);
	tcon_mipi_dsi_if_t *dsi_if;
	u32 tcon_id;
	bool slave;

	if (!vo_dev_cfg) {
		DRM_ERROR("null vo_dev_cfg info\n");
		return;
	}

	dsi_if = &vo_dev_cfg->dsi_if;
	tcon_id = dsi_if->tcon_host->idx;
	slave = (dsi->id != tcon_id) ? true : false;
	dsi->configs.dsi_if.tcon_host = dsi_if->tcon_host;
	dsi->configs.dsi_if.is_slave = slave;
	DRM_DEBUG_KMS("dsi%d route from tcon%d as %s\n", dsi->id, tcon_id,
		slave ? "slave" : "non-slave");
}

static void lombo_dsi_set_clk(struct lombo_mipi_dsi *dsi, int enable, int sw)
{
	struct lombo_vo_dev_config *vo = &dsi->configs;
	tcon_mipi_dsi_if_t *dsi_if = &dsi->configs.dsi_if;
	struct lombo_doss_clk *ref_clk = dsi->ref_clk;
	struct lombo_doss_clk *cfg_clk = dsi->cfg_clk;
	unsigned long best_rate, req_rate;
	uint64_t tmp;
	uint32_t rem;

	if (!enable) {
		lombo_doss_clk_disable(ref_clk);
		lombo_doss_clk_disable(cfg_clk);
		lombo_doss_path_set(dsi->doss_top, dsi->id,
				DOSS_CTRL_MIPI_DSI, 0);
		return;
	}

	if (sw) {
		lombo_doss_clk_enable(ref_clk, 1);
		lombo_doss_clk_enable(cfg_clk, 1);
		return;
	}
	lombo_doss_path_set(dsi->doss_top, dsi->id, DOSS_CTRL_MIPI_DSI, 1);

	req_rate = (unsigned long)vo->timing.dclk_freq;
	lombo_doss_clk_set_rate(ref_clk, req_rate);
	lombo_doss_get_best_sclk_rate(ref_clk,
		DOSS_CLK_MIPI_DSI_REF, &best_rate);

	tmp = best_rate;
	rem = do_div(tmp, req_rate);
	if (rem > (uint32_t)(req_rate + 1) >> 1)
		tmp++;
	if (tmp == 0)
		tmp++;
	ref_clk->div0 = (uint32_t)tmp;
	ref_clk->div1 = 1;
	tmp = best_rate;
	do_div(tmp, ref_clk->div0);

	cfg_clk->div0 = cfg_clk->div1 = 1;
	lombo_doss_clk_enable(ref_clk, 0);
	lombo_doss_clk_enable(cfg_clk, 0);

	dsi_if->dsi_ref_freq = lombo_doss_clk_get_rate(ref_clk);
	dsi_if->dsi_ref_div = lombo_doss_clk_get_div(ref_clk);
}

static void lombo_dsi_encoder_modeset(struct drm_encoder *enc)
{
	struct lombo_mipi_dsi *dsi = to_lombo_mipi_dsi_by_enc(enc);
	tcon_mipi_dsi_if_t *dsi_if = &dsi->configs.dsi_if;

	if (lombo_disp_boot_sync(enc->crtc)) {
		struct drm_panel *p = dsi->panel;

		lombo_dsi_set_clk(dsi, 1, 1);
		if (p && p->funcs && p->funcs->boot_sync)
			p->funcs->boot_sync(p, LOMBO_PANEL_BOOT_SYNC_PREPARE);
		return;
	}
	DRM_DEBUG_ATOMIC("%s\n", __func__);

	lombo_dsi_set_clk(dsi, 1, 0);
	lombo_doss_vref_config(dsi->doss_top, dsi->id, DOSS_CTRL_MIPI_DSI, 1);
	if (dsi_if->is_slave)
		lombo_doss_slave_config(dsi->doss_top,
			dsi->id, DOSS_CTRL_MIPI_DSI, 1);

	if (dsi->lanes != 4)
		lombo_doss_phy_slave_config(dsi->doss_top,
			0, DOSS_CTRL_MIPI_DSI, 1);

	if (csp_mipi_dsi_init(dsi->id, dsi_if))
		DRM_ERROR("csp_mipi_dsi_init fail\n");
	csp_mipi_dsi_cfg(dsi->id, dsi_if);

	drm_panel_prepare(dsi->panel);

	csp_mipi_dsi_start(dsi->id);
}

static void lombo_dsi_encoder_enable(struct drm_encoder *enc)
{
	struct lombo_mipi_dsi *dsi = to_lombo_mipi_dsi_by_enc(enc);

	lombo_dsi_encoder_modeset(enc);

	if (lombo_disp_boot_sync(enc->crtc)) {
		struct drm_panel *p = dsi->panel;

		if (p && p->funcs && p->funcs->boot_sync)
			p->funcs->boot_sync(p, LOMBO_PANEL_BOOT_SYNC_ENABLE);
		return;
	}

	DRM_DEBUG_ATOMIC("%s\n", __func__);
	drm_panel_enable(dsi->panel);
}

static void lombo_dsi_encoder_disable(struct drm_encoder *enc)
{
	struct lombo_mipi_dsi *dsi = to_lombo_mipi_dsi_by_enc(enc);

	DRM_DEBUG_ATOMIC("%s\n", __func__);

	drm_panel_disable(dsi->panel);
	drm_panel_unprepare(dsi->panel);

	csp_mipi_dsi_stop(dsi->id);
	csp_mipi_dsi_reset(dsi->id);
	lombo_doss_vref_config(dsi->doss_top, dsi->id, DOSS_CTRL_MIPI_DSI, 0);
	if (dsi->configs.dsi_if.is_slave)
		lombo_doss_slave_config(dsi->doss_top,
			dsi->id, DOSS_CTRL_MIPI_DSI, 0);
	if (dsi->lanes != 4)
		lombo_doss_phy_slave_config(dsi->doss_top,
			0, DOSS_CTRL_MIPI_DSI, 0);
	lombo_dsi_set_clk(dsi, 0, 0);
}

static const struct drm_encoder_helper_funcs lombo_dsi_enc_helper_funcs = {
	.atomic_check		= lombo_dsi_encoder_atomic_check,
	.atomic_mode_set	= lombo_dsi_encoder_atomic_mode_set,
	.enable			= lombo_dsi_encoder_enable,
	.disable		= lombo_dsi_encoder_disable,
};

static const struct drm_encoder_funcs lombo_dsi_enc_funcs = {
	.destroy = drm_encoder_cleanup,
};

int lombo_dsi_encoder_get_clk(struct drm_encoder *enc,
	uint32_t num, struct lombo_doss_clk **p_clks)
{
	struct lombo_mipi_dsi *dsi = to_lombo_mipi_dsi_by_enc(enc);

	if (!enc || (num < 2) || !p_clks) {
		DRM_ERROR("null pointer or num=%d\n", num);
		return 0;
	}

	*p_clks++ = dsi->ref_clk;
	*p_clks++ = dsi->cfg_clk;
	return 2;
}
EXPORT_SYMBOL(lombo_dsi_encoder_get_clk);

static int lombo_dsi_connector_get_modes(
	struct drm_connector *con)
{
	struct lombo_mipi_dsi *dsi = to_lombo_mipi_dsi_by_con(con);

	DRM_DEBUG_ATOMIC("%s\n", __func__);
	return drm_panel_get_modes(dsi->panel);
}

static struct drm_connector_helper_funcs lombo_dsi_con_helper_funcs = {
	.get_modes = lombo_dsi_connector_get_modes,
};

static enum drm_connector_status lombo_dsi_connector_detect(
	struct drm_connector *con, bool force)
{
	DRM_DEBUG_ATOMIC("%s\n", __func__);

	return con->status;
}

static struct drm_connector_funcs lombo_dsi_con_funcs = {
	.detect			= lombo_dsi_connector_detect,
	.reset			= drm_atomic_helper_connector_reset,
	.fill_modes		= drm_helper_probe_single_connector_modes,
	.destroy		= drm_connector_cleanup,
	.atomic_duplicate_state	= drm_atomic_helper_connector_duplicate_state,
	.atomic_destroy_state	= drm_atomic_helper_connector_destroy_state,
};

int lombo_mipi_dsi_attach(struct mipi_dsi_host *host,
	      struct mipi_dsi_device *dsi)
{
	struct lombo_mipi_dsi *ldsi = to_lombo_mipi_dsi_by_host(host);
	int ret;

	DRM_DEBUG_DRIVER("%d\n", __LINE__);

	ldsi->lanes = dsi->lanes;
	ldsi->format = dsi->format;
	ldsi->flag = dsi->mode_flags;

	ldsi->panel = of_drm_find_panel(dsi->dev.of_node);
	if (IS_ERR_OR_NULL(ldsi->panel)) {
		ldsi->panel = NULL;
		DRM_DEBUG("fail find dsi panel\n");

		ldsi->bridge = of_drm_find_bridge(dsi->dev.of_node);
		if (IS_ERR_OR_NULL(ldsi->bridge)) {
			ldsi->bridge = NULL;
			DRM_DEBUG("fail find dsi bridge\n");
		}
	}

	if (ldsi->panel || ldsi->bridge) {
		u32 possible_crtcs = 0;

		drm_encoder_helper_add(&ldsi->encoder,
				&lombo_dsi_enc_helper_funcs);
		ret = drm_encoder_init(ldsi->drm, &ldsi->encoder,
				&lombo_dsi_enc_funcs,
				DRM_MODE_ENCODER_DSI, NULL);
		if (ret) {
			DRM_ERROR("dsi encoder init fail\n");
			return ret;
		}
		possible_crtcs = (1 << ldsi->drm->mode_config.num_crtc) - 1;
		ldsi->encoder.possible_crtcs = possible_crtcs;
	} else {
		ldsi->connector.status = connector_status_disconnected;
	}

	if (ldsi->panel) {
		DRM_INFO("attaching dsi panel\n");
			ldsi->connector.dpms = DRM_MODE_DPMS_OFF;

		drm_connector_attach_encoder(
			&ldsi->connector, &ldsi->encoder);
		drm_panel_attach(ldsi->panel, &ldsi->connector);
	}

	if (ldsi->bridge) {
		DRM_INFO("attaching dsi bridge\n");
		drm_connector_cleanup(&ldsi->connector);
		drm_bridge_attach(&ldsi->encoder, ldsi->bridge, NULL);
	}

	if (ldsi->drm->mode_config.poll_enabled)
		drm_kms_helper_hotplug_event(ldsi->drm);

	return 0;
}
int lombo_mipi_dsi_detach(struct mipi_dsi_host *host,
	      struct mipi_dsi_device *dsi)
{
	struct lombo_mipi_dsi *ldsi = to_lombo_mipi_dsi_by_host(host);

	DRM_DEBUG_DRIVER("%d\n", __LINE__);

	if (ldsi->panel) {
		drm_panel_detach(ldsi->panel);
		ldsi->connector.status = connector_status_disconnected;
	}

	if (ldsi->drm->mode_config.poll_enabled)
		drm_kms_helper_hotplug_event(ldsi->drm);

	return 0;
}

static ssize_t lombo_mipi_dsi_host_transfer(
	struct mipi_dsi_host *host,
	const struct mipi_dsi_msg *msg)
{
	struct lombo_mipi_dsi *dsi = to_lombo_mipi_dsi_by_host(host);
	u8 *buf = (u8 *)msg->tx_buf;

	switch (msg->type) {
	case MIPI_DSI_DCS_SHORT_WRITE:
		csp_mipi_dsi_dcs_sw_0p(dsi->id, msg->channel, buf[0]);
		break;
	case MIPI_DSI_DCS_SHORT_WRITE_PARAM:
		csp_mipi_dsi_dcs_sw_1p(dsi->id, msg->channel, buf[0], buf[1]);
		break;
	case MIPI_DSI_DCS_LONG_WRITE:
		csp_mipi_dsi_dcs_lw(dsi->id, msg->channel, buf, msg->tx_len);
		break;
	default:
		DRM_ERROR("msg->type=0x%x\n", msg->type);
		break;
	}

	return 0;
}

static struct mipi_dsi_host_ops lombo_mipi_dsi_host_ops = {
	.attach = lombo_mipi_dsi_attach,
	.detach = lombo_mipi_dsi_detach,
	.transfer = lombo_mipi_dsi_host_transfer,
};

static inline void lombo_set_csp_mipi_dsi_version(void)
{
	u32 version = CSP_MIPI_DSI_VERSION_V10;

	csp_mipi_dsi_set_version(version);
}

static int lombo_mipi_dsi_bind(struct device *dev,
	struct device *master, void *master_data)
{
	struct device_node *np, *remote;
	struct platform_device *pdev, *doss_top_pdev;
	struct drm_device *drm = master_data;
	struct lombo_mipi_dsi *dsi;
	struct resource *res;
	u32 value = 0;
	int ret;

	DRM_DEBUG_DRIVER("%d\n", __LINE__);

	np = dev->of_node;
	pdev = to_platform_device(dev);

	remote = of_parse_phandle(dev->of_node, "top", 0);
	if (!remote) {
		DRM_ERROR("failed to parse phandle of top\n");
		return -EINVAL;
	}
	doss_top_pdev = of_find_device_by_node(remote);
	if (!doss_top_pdev) {
		DRM_ERROR("failed to find device doss top\n");
		return -EINVAL;
	}

	dsi = kzalloc(sizeof(*dsi), GFP_KERNEL);
	if (!dsi) {
		dev_err(dev, "alloc for tcon failed.\n");
		return -ENOMEM;
	}
	dsi->dev = dev;
	dsi->doss_top = &doss_top_pdev->dev;
	dsi->drm = drm;

	/* parse hardware index */
	if (!of_property_read_u32(np, "hw_id", &value))
		dsi->id = value;
	else
		DRM_INFO("get dsi hw_id failed\n");

	/* parse reg addr */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (res == NULL) {
		dev_err(dev, "get memory resource failed.\n");
		return -ENXIO;
	}
	dsi->reg = devm_ioremap(dev, res->start,
				resource_size(res));
	if (dsi->reg == NULL) {
		dev_err(dev, "register mapping failed.\n");
		return -ENXIO;
	}
	lombo_set_csp_mipi_dsi_version();
	csp_mipi_dsi_set_register_base(dsi->id, dsi->reg,
		(u32)resource_size(res));

	dsi->cfg_clk = lombo_doss_get_clk(dev,
				&doss_top_pdev->dev,
				dsi->id, DOSS_CLK_MIPI_DSI_CFG);
	dsi->ref_clk = lombo_doss_get_clk(dev,
				&doss_top_pdev->dev,
				dsi->id, DOSS_CLK_MIPI_DSI_REF);
	if (!dsi->cfg_clk || !dsi->ref_clk) {
		DRM_ERROR("doss_get_clk for dsi fail. cfg=%p,ref=%p\n",
			dsi->cfg_clk, dsi->ref_clk);
		return -EINVAL;
	}

	drm_connector_helper_add(&dsi->connector, &lombo_dsi_con_helper_funcs);
	ret = drm_connector_init(dsi->drm,
		&dsi->connector, &lombo_dsi_con_funcs, DRM_MODE_CONNECTOR_DSI);
	if (ret) {
		DRM_ERROR("dsi connector init fail\n");
		return ret;
	}
	dsi->connector.status = connector_status_connected;
	dsi->connector.polled = DRM_CONNECTOR_POLL_HPD;

	dsi->host.dev = dsi->dev;
	dsi->host.ops = &lombo_mipi_dsi_host_ops;
	mipi_dsi_host_register(&dsi->host);

	dev_set_drvdata(dev, dsi);

	return 0;
}

static void lombo_mipi_dsi_unbind(struct device *dev,
	struct device *master, void *master_data)
{
	struct lombo_mipi_dsi *dsi = dev_get_drvdata(dev);

	DRM_DEBUG_DRIVER("%d\n", __LINE__);

	if (dsi->bridge)
		drm_bridge_detach(dsi->bridge);
	mipi_dsi_host_unregister(&dsi->host);

	lombo_doss_put_clk(&dsi->ref_clk);
	lombo_doss_put_clk(&dsi->cfg_clk);
	csp_mipi_dsi_set_register_base(dsi->id, NULL, 0);
	kfree(dsi);
}

static const struct component_ops lombo_mipi_dsi_ops = {
	.bind	= lombo_mipi_dsi_bind,
	.unbind	= lombo_mipi_dsi_unbind,
};

static int lombo_mipi_dsi_probe(struct platform_device *pdev)
{
	return component_add(&pdev->dev, &lombo_mipi_dsi_ops);
}

static int lombo_mipi_dsi_remove(struct platform_device *pdev)
{
	component_del(&pdev->dev, &lombo_mipi_dsi_ops);
	return 0;
}

static const struct of_device_id lombo_mipi_dsi_of_table[] = {
	{ .compatible = "lombo,lombo-n7v1-mipi-dsi" },
	{ .compatible = "lombo,lombo-n7v3-mipi-dsi" },
	{ .compatible = "lombo,lombo-n9v1-mipi-dsi-0" },
	{ .compatible = "lombo,lombo-n9v1-mipi-dsi-1" },
	{ }
};
MODULE_DEVICE_TABLE(of, lombo_mipi_dsi_of_table);

static struct platform_driver lombo_mipi_dsi_platform_driver = {
	.probe		= lombo_mipi_dsi_probe,
	.remove		= lombo_mipi_dsi_remove,
	.driver		= {
		.owner = THIS_MODULE,
		.name		= "lombo-mipi-dsi",
		.of_match_table	= lombo_mipi_dsi_of_table,
	},
};
module_platform_driver(lombo_mipi_dsi_platform_driver);

MODULE_AUTHOR("lomboswer <lomboswer@lombotech.com>");
MODULE_DESCRIPTION("Lombo SoC DOSS MIPI DSI Driver");
MODULE_LICENSE("GPL");
